using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;

namespace Microsoft.Maui.Controls
{

	internal abstract class ShellElementCollection :
		IList<BaseShellItem>,
		INotifyCollectionChanged
	{
		public event NotifyCollectionChangedEventHandler VisibleItemsChangedInternal;
		readonly List<NotifyCollectionChangedEventArgs> _notifyCollectionChangedEventArgs;
		bool _pauseCollectionChanged;
		public event NotifyCollectionChangedEventHandler CollectionChanged;
		public event NotifyCollectionChangedEventHandler VisibleItemsChanged;
		public int Count => Inner.Count;
		public bool IsReadOnly => Inner.IsReadOnly;
		IList _inner;
		IList _visibleItems;

		protected ShellElementCollection()
		{
			_notifyCollectionChangedEventArgs = new List<NotifyCollectionChangedEventArgs>();
		}

		internal IList Inner
		{
			get => _inner;
			private protected set
			{
				if (_inner != null)
					throw new ArgumentException("Inner can only be set once");

				_inner = value;
				((INotifyCollectionChanged)_inner).CollectionChanged += InnerCollectionChanged;
			}
		}

		protected void OnVisibleItemsChanged(object sender, NotifyCollectionChangedEventArgs args)
		{
			if (args?.NewItems?.Count > 0 && _pauseCollectionChanged)
			{
				_notifyCollectionChangedEventArgs.Add(args);
				ResumeCollectionChanged();
				return;
			}

			if (_pauseCollectionChanged)
			{
				_notifyCollectionChangedEventArgs.Add(args);
				return;
			}

			VisibleItemsChangedInternal?.Invoke(VisibleItemsReadOnly, args);
			VisibleItemsChanged?.Invoke(VisibleItemsReadOnly, args);
		}

		protected IList VisibleItems
		{
			get => _visibleItems;
			private protected set
			{
				_visibleItems = value;
				((INotifyCollectionChanged)_visibleItems).CollectionChanged += OnVisibleItemsChanged;
			}
		}

		public IReadOnlyCollection<BaseShellItem> VisibleItemsReadOnly
		{
			get;
			private protected set;
		}

		// Pause Collection Changed events when the list has zero items
		// we don't want to propagate out a visible collection changed event until the next visible item
		// is realized
		void PauseCollectionChanged() => _pauseCollectionChanged = true;

		void ResumeCollectionChanged()
		{
			_pauseCollectionChanged = false;

			// process the added items first and then remove
			var pendingEvents = _notifyCollectionChangedEventArgs.OrderBy(x => x.NewItems != null ? 0 : 1).ToList();
			_notifyCollectionChangedEventArgs.Clear();

			foreach (var args in pendingEvents)
				VisibleItemsChangedInternal?.Invoke(VisibleItemsReadOnly, args);

			foreach (var args in pendingEvents)
				VisibleItemsChanged?.Invoke(VisibleItemsReadOnly, args);
		}

		#region IList

		public BaseShellItem this[int index]
		{
			get => (BaseShellItem)Inner[index];
			set => Inner[index] = value;
		}

		public void Clear()
		{
			var list = Inner.Cast<BaseShellItem>().ToList();
			try
			{
				PauseCollectionChanged();
				Removing(Inner);
			}
			finally
			{
				ResumeCollectionChanged();
			}

			Inner.Clear();
			CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, list));
		}

		public virtual void Add(BaseShellItem item) => Inner.Add(item);

		public virtual bool Contains(BaseShellItem item) => Inner.Contains(item);

		public virtual void CopyTo(BaseShellItem[] array, int arrayIndex) => Inner.CopyTo(array, arrayIndex);

		public abstract IEnumerator<BaseShellItem> GetEnumerator();

		public virtual int IndexOf(BaseShellItem item) => Inner.IndexOf(item);

		public virtual void Insert(int index, BaseShellItem item) => Inner.Insert(index, item);

		public abstract bool Remove(BaseShellItem item);

		public virtual void RemoveAt(int index) => Inner.RemoveAt(index);

		IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Inner).GetEnumerator();

		#endregion

		void InnerCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
		{
			if (e.NewItems != null)
			{
				foreach (BaseShellItem element in e.NewItems)
				{
					if (element is IElementController controller)
						OnElementControllerInserting(controller);

					CheckVisibility(element);
				}
			}

			if (e.OldItems != null)
			{
				Removing(e.OldItems);
			}

			CollectionChanged?.Invoke(this, e);
		}

		void Removing(IEnumerable items)
		{
			foreach (BaseShellItem element in items)
			{
				if (VisibleItems.Contains(element))
					VisibleItems.Remove(element);

				if (element is IElementController controller)
					OnElementControllerRemoving(controller);
			}
		}

		protected void CheckVisibility(BaseShellItem element)
		{
			if (IsBaseShellItemVisible(element))
			{
				if (VisibleItems.Contains(element))
					return;

				int visibleIndex = 0;
				for (var i = 0; i < Inner.Count; i++)
				{
					var item = Inner[i];

					if (!IsBaseShellItemVisible(element))
						continue;

					if (item == element)
					{
						VisibleItems.Insert(visibleIndex, element);
						break;
					}

					if (VisibleItems.Contains(item))
						visibleIndex++;
				}
			}
			else if (VisibleItems.Contains(element))
			{
				VisibleItems.Remove(element);
			}

			bool IsBaseShellItemVisible(BaseShellItem item)
			{
				if (!item.IsVisible)
					return false;

				if (item is ShellGroupItem sgi)
				{
					return (sgi.ShellElementCollection.VisibleItemsReadOnly.Count > 0) ||
						item is IMenuItemController;
				}

				return IsShellElementVisible(item);
			}
		}

		protected virtual bool IsShellElementVisible(BaseShellItem item)
		{
			return false;
		}


		protected virtual void OnElementControllerInserting(IElementController controller)
		{
			if (controller is ShellGroupItem sgi)
			{
				sgi.ShellElementCollection.VisibleItemsChangedInternal += OnShellElementControllerItemsCollectionChanged;
			}

			if (controller is BaseShellItem bsi)
				bsi.PropertyChanged += BaseShellItemPropertyChanged;
		}

		protected virtual void OnElementControllerRemoving(IElementController controller)
		{
			if (controller is ShellGroupItem sgi)
			{
				sgi.ShellElementCollection.VisibleItemsChangedInternal -= OnShellElementControllerItemsCollectionChanged;
			}

			if (controller is BaseShellItem bsi)
				bsi.PropertyChanged -= BaseShellItemPropertyChanged;
		}

		void BaseShellItemPropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			if (e.PropertyName == nameof(BaseShellItem.IsVisible))
				CheckVisibility((BaseShellItem)sender);
		}

		void OnShellElementControllerItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
		{
			foreach (BaseShellItem bsi in (e.NewItems ?? e.OldItems ?? Inner))
			{
				if (bsi.Parent == null)
					bsi.ParentSet += OnParentSet;
				else
					CheckVisibility(bsi.Parent as BaseShellItem);
			}

			void OnParentSet(object s, EventArgs __)
			{
				var baseShellItem = (BaseShellItem)s;
				baseShellItem.ParentSet -= OnParentSet;
				CheckVisibility(baseShellItem.Parent as BaseShellItem);
			}
		}

	}

	internal abstract class ShellElementCollection<TBaseShellItem> :
		ShellElementCollection,
		IList<TBaseShellItem>
		where TBaseShellItem : BaseShellItem
	{

		public ShellElementCollection()
		{
			var items = new ObservableCollection<TBaseShellItem>();
			VisibleItems = items;
			VisibleItemsReadOnly = new ReadOnlyCollection<TBaseShellItem>(items);
		}

		public new ReadOnlyCollection<TBaseShellItem> VisibleItemsReadOnly
		{
			get => (ReadOnlyCollection<TBaseShellItem>)base.VisibleItemsReadOnly;
			private protected set => base.VisibleItemsReadOnly = value;
		}

		internal new IList<TBaseShellItem> Inner
		{
			get => (IList<TBaseShellItem>)base.Inner;
			set => base.Inner = (IList)value;
		}


		TBaseShellItem IList<TBaseShellItem>.this[int index]
		{
			get => (TBaseShellItem)Inner[index];
			set => Inner[index] = value;
		}

		public virtual void Add(TBaseShellItem item) => Inner.Add(item);

		public virtual bool Contains(TBaseShellItem item) => Inner.Contains(item);

		public virtual void CopyTo(TBaseShellItem[] array, int arrayIndex) => Inner.CopyTo(array, arrayIndex);

		public virtual int IndexOf(TBaseShellItem item) => Inner.IndexOf(item);

		public virtual void Insert(int index, TBaseShellItem item) => Inner.Insert(index, item);

		public virtual bool Remove(TBaseShellItem item) => Inner.Remove(item);

		IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Inner).GetEnumerator();

		IEnumerator<TBaseShellItem> IEnumerable<TBaseShellItem>.GetEnumerator()
		{
			return Inner.GetEnumerator();
		}

		public override IEnumerator<BaseShellItem> GetEnumerator()
		{
			return Inner.Cast<BaseShellItem>().GetEnumerator();
		}

		public override bool Remove(BaseShellItem item)
		{
			return Remove((TBaseShellItem)item);
		}
	}
}
